import {
  BufferGeometry,
  CatmullRomCurve3,
  Float32BufferAttribute,
  MathUtils,
  Matrix4,
  Vector2,
  Vector3
} from 'three'

export class TunnelGeometry extends BufferGeometry {
  constructor(
    path = new CatmullRomCurve3([
      new Vector3(-1, -1, 0),
      new Vector3(-1, 1, 0),
      new Vector3(1, 1, 0)
    ]),
    uMappingFrames = undefined,
    closed = false,
    tunnelType = 'circle',
    width = 4,
    height = 4,
    radialSegments = 12
  ) {
    super()

    this.type = 'TunnelGeometry'

    if (tunnelType === 'rectangle') {
      radialSegments = 4
    } else if (tunnelType === 'trapezoid') {
      radialSegments = 6
    }

    this.parameters = {
      tunnelType: tunnelType,
      path: path,
      uMappingFrames: uMappingFrames,
      width: width,
      height: height,
      radialSegments: radialSegments,
      closed: closed
    }

    // 获取path的弧长数组
    if (!uMappingFrames) {
      uMappingFrames = TunnelGeometry.pathToUMapping(path)
    }

    const frames = this.computeFrenetFrames(path, uMappingFrames, closed)
    // expose internals

    this.tangents = frames.tangents
    this.normals = frames.normals
    this.binormals = frames.binormals

    // helper variables

    const vertex = new Vector3()
    const normal = new Vector3()
    const uv = new Vector2()
    let P = new Vector3()

    // buffer

    const vertices = []
    const normals = []
    const uvs = []
    const indices = []

    // create buffer data
    const scope = this

    generateBufferData()

    // build geometry

    this.setIndex(indices)
    this.setAttribute('position', new Float32BufferAttribute(vertices, 3))
    this.setAttribute('normal', new Float32BufferAttribute(normals, 3))
    this.setAttribute('uv', new Float32BufferAttribute(uvs, 2))
    // functions

    function generateBufferData() {
      for (let i = 0; i < uMappingFrames.length - 1; i++) {
        generateSegment(i)
        generateGroups(i)
      }

      // if the geometry is not closed, generate the last row of vertices and normals
      // at the regular position on the given path
      //
      // if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)

      generateSegment(closed === false ? uMappingFrames.length - 1 : 0)

      // uvs are generated in a separate function.
      // this makes it easy compute correct values for closed geometries

      generateUVs()

      // finally create faces

      generateIndices()
    }

    function generateSegment(i) {
      // we use getPointAt to sample evenly distributed points from the given path

      P = path.getPointAt(uMappingFrames[i])

      // retrieve corresponding normal and binormal

      const N = frames.normals[i]
      const B = frames.binormals[i]

      // generate normals and vertices for the current segment
      const direction = getDirection()
      const widthHalf = width / 2
      // console.log(direction)
      const sqrtValue = Math.sqrt(Math.pow(widthHalf, 2) + Math.pow(height - widthHalf, 2))
      for (let j = 0; j <= radialSegments; j++) {
        let v = (j / radialSegments) * Math.PI * 2
        let d = 0

        if (tunnelType === 'arch' || tunnelType === 'trapezoid') {
          v = (j / (radialSegments - 3)) * Math.PI

          if (j === radialSegments - 2) {
            v = Math.PI + Math.PI / 4
          }
          if (j === radialSegments - 1) {
            v = Math.PI + (Math.PI / 4) * 3
          }
          if (j === radialSegments) {
            v = Math.PI * 2
          }

          if (['x', 'y'].includes(direction)) {
            d = -Math.PI
          } else if (direction === 'z') {
            d = Math.PI / 2
          }
        } else if (tunnelType === 'rectangle') {
          if (['x', 'y'].includes(direction)) {
            d = (-Math.PI / 4) * 3
          } else if (['-x', '-y'].includes(direction)) {
            d = Math.PI / 4
          } else if (direction === 'z') {
            d = (Math.PI / 4) * 3
          }
        }

        const sin = Math.sin(v + d)
        const cos = -Math.cos(v + d)

        // normal
        normal.x = cos * N.x + sin * B.x
        normal.y = cos * N.y + sin * B.y
        normal.z = cos * N.z + sin * B.z
        normal.normalize()

        normals.push(normal.x, normal.y, normal.z)

        // vertex
        vertex.x = P.x + widthHalf * normal.x
        vertex.y = P.y + widthHalf * normal.y
        vertex.z = P.z + widthHalf * normal.z

        if (tunnelType === 'arch' || tunnelType === 'trapezoid') {
          if (j === radialSegments - 2 || j === radialSegments - 1) {
            vertex.x = P.x + sqrtValue * normal.x
            vertex.y = P.y + sqrtValue * normal.y
            vertex.z = P.z + sqrtValue * normal.z
          }
        }

        vertices.push(vertex.x, vertex.y, vertex.z)
      }
    }

    function generateIndices() {
      for (let j = 1; j <= uMappingFrames.length - 1; j++) {
        for (let i = 1; i <= radialSegments; i++) {
          const a = (radialSegments + 1) * (j - 1) + (i - 1)
          const b = (radialSegments + 1) * j + (i - 1)
          const c = (radialSegments + 1) * j + i
          const d = (radialSegments + 1) * (j - 1) + i

          // faces

          indices.push(a, b, d)
          indices.push(b, c, d)
        }
      }
    }

    function generateUVs() {
      for (let i = 0; i <= uMappingFrames.length - 1; i++) {
        for (let j = 0; j <= radialSegments; j++) {
          uv.x = i / (uMappingFrames.length - 1)
          // uv.x = i / 1
          uv.y = j / (radialSegments - 3)
          if (j < radialSegments - 2) {
            uvs.push(uv.x, uv.y)
          } else if (j === radialSegments - 2) {
            uvs.push(uv.x, 0)
          } else if (j === radialSegments - 1) {
            uvs.push(uv.x, 1)
          } else if (j === radialSegments) {
            uvs.push(uv.x, 0)
          }
        }
      }
    }

    function generateGroups(i) {
      scope.addGroup(i * radialSegments * 6, (radialSegments - 3) * 6, 0)
      scope.addGroup((radialSegments - 3) * 6 + radialSegments * 6 * i, 6, 1)
      scope.addGroup((radialSegments - 3) * 6 + radialSegments * 6 * i + 6, 6, 2)
      scope.addGroup((radialSegments - 3) * 6 + radialSegments * 6 * i + 12, 6, 1)
    }

    function getDirection() {
      const { x, y, z } = path.getTangent(0.1)
      if (Math.abs(x) > Math.abs(y) && Math.abs(x) > Math.abs(z)) {
        if (x < 0) {
          return '-x'
        }
        return 'x'
      } else if (Math.abs(y) > Math.abs(z)) {
        if (y < 0) {
          return '-y'
        }
        return 'y'
      } else {
        return 'z'
      }
    }
  }

  computeFrenetFrames(path, uMappingFrames, closed) {
    const normal = new Vector3()

    const tangents = []
    const normals = []
    const binormals = []

    const vec = new Vector3()
    const mat = new Matrix4()

    const segments = uMappingFrames.length - 1

    // compute the tangent vectors for each segment on the curve

    for (let i = 0; i <= segments; i++) {
      const u = uMappingFrames[i]

      tangents[i] = path.getTangentAt(u)
    }
    // select an initial normal vector perpendicular to the first tangent vector,
    // and in the direction of the minimum tangent xyz component

    normals[0] = new Vector3()
    binormals[0] = new Vector3()
    let min = Number.MAX_VALUE
    const tx = Math.abs(tangents[0].x)
    const ty = Math.abs(tangents[0].y)
    const tz = Math.abs(tangents[0].z)

    if (tx <= min) {
      min = tx
      normal.set(1, 0, 0)
    }

    if (ty <= min) {
      min = ty
      normal.set(0, 1, 0)
    }

    if (tz <= min) {
      normal.set(0, 0, 1)
    }

    vec.crossVectors(tangents[0], normal).normalize()

    normals[0].crossVectors(tangents[0], vec)
    binormals[0].crossVectors(tangents[0], normals[0])

    // compute the slowly-varying normal and binormal vectors for each segment on the curve

    for (let i = 1; i <= segments; i++) {
      normals[i] = normals[i - 1].clone()

      binormals[i] = binormals[i - 1].clone()

      vec.crossVectors(tangents[i - 1], tangents[i])

      if (vec.length() > Number.EPSILON) {
        vec.normalize()

        const theta = Math.acos(MathUtils.clamp(tangents[i - 1].dot(tangents[i]), -1, 1)) // clamp for floating pt errors

        normals[i].applyMatrix4(mat.makeRotationAxis(vec, theta))
      }

      binormals[i].crossVectors(tangents[i], normals[i])
    }

    // if the curve is closed, postprocess the vectors so the first and last normal vectors are the same

    if (closed === true) {
      let theta = Math.acos(MathUtils.clamp(normals[0].dot(normals[segments]), -1, 1))
      theta /= segments

      if (tangents[0].dot(vec.crossVectors(normals[0], normals[segments])) > 0) {
        theta = -theta
      }
      for (let i = 1; i <= segments; i++) {
        // twist a little...
        normals[i].applyMatrix4(mat.makeRotationAxis(tangents[i], theta * i))
        binormals[i].crossVectors(tangents[i], normals[i])
      }
    }
    return {
      tangents: tangents,
      normals: normals,
      binormals: binormals
    }
  }

  toJSON() {
    const data = super.toJSON()

    data.path = this.parameters.path.toJSON()

    return data
  }

  static fromJSON(data) {
    const vecs = data.path.points.map((item) => {
      return new Vector3(...item)
    })
    const path = new CatmullRomCurve3(vecs)
    const geo = new TunnelGeometry(path, TunnelGeometry.pathToUMapping(path, 3, 0.3))
    geo.center()
    return geo
  }

  // 用于补充拐点并生成所有拐点的弧长数组
  static pathToUMapping(
    path = new CatmullRomCurve3([
      new Vector3(-1, -1, 0),
      new Vector3(-1, 1, 0),
      new Vector3(1, 1, 0)
    ]),
    elbowSegmentNum = 1,
    elbowSegmentOffset = 1
  ) {
    const lengths = [0]
    path.points.forEach((p, i, arr) => {
      if (i > 0) {
        const last = lengths.at(-1) // 获取到最后一个
        const dist = p.distanceTo(arr[i - 1]) // 计算当前点和上一个点的距离
        const next = last + dist // 计算下一个
        // 当距离过小时，不能在补充拐点
        const numElbow = Math.min(elbowSegmentNum, dist / 2 / elbowSegmentOffset - 1) // 计算需要多少个elbow
        if (i > 1) {
          for (let j = 1; j <= numElbow; ++j) {
            lengths.push(last + j * elbowSegmentOffset) // 计算每个elbow
          }
        }
        if (i < arr.length - 1) {
          for (let j = numElbow; j >= 1; --j) {
            lengths.push(next - j * elbowSegmentOffset)
          }
        }
        lengths.push(next)
      }
    })

    const uMappingFrames = lengths.map((l) => l / lengths.at(-1))

    return uMappingFrames
  }

  static removeTopFromTunnelGeometry(geometry, radialSegments = 8) {
    const tunnelType = geometry.parameters.tunnelType
    if (tunnelType === 'rectangle') {
      radialSegments = 4
    } else if (tunnelType === 'trapezoid') {
      radialSegments = 6
    }
    const positionAttribute = geometry.getAttribute('position')
    const normalAttribute = geometry.getAttribute('normal')
    const uvAttribute = geometry.getAttribute('uv')
    const indexAttribute = geometry.getIndex()

    const vertexCountPerSegment = radialSegments + 1
    const segmentCount = positionAttribute.count / vertexCountPerSegment
    // console.log(segmentCount)
    // 确定顶部顶点范围
    const topVertexStart = vertexCountPerSegment - 5
    // 存储新的顶点、法线、纹理坐标和索引
    const newPositions = []
    const newNormals = []
    const newUVs = []
    const newIndices = []

    // 用于映射旧索引到新索引
    const indexMap = new Map()
    let newIndex = 0
    geometry.clearGroups()

    // 遍历每个分段，移除顶部顶点并更新索引映射
    for (let s = 0; s < segmentCount; s++) {
      for (let v = 0; v < vertexCountPerSegment; v++) {
        if (v > topVertexStart) {
          const baseIndex = s * vertexCountPerSegment + v
          newPositions.push(
            positionAttribute.getX(baseIndex),
            positionAttribute.getY(baseIndex),
            positionAttribute.getZ(baseIndex)
          )
          newNormals.push(
            normalAttribute.getX(baseIndex),
            normalAttribute.getY(baseIndex),
            normalAttribute.getZ(baseIndex)
          )
          newUVs.push(uvAttribute.getX(baseIndex), uvAttribute.getY(baseIndex))
          indexMap.set(baseIndex, newIndex++)
        }
      }

      geometry.addGroup(s * 6 + s * 12, 6, 0)
      geometry.addGroup(6 * (s + 1) + s * 12, 6, 1)
      geometry.addGroup(6 * (s + 2) + s * 12, 6, 0)
    }

    // 更新索引数据
    for (let i = 0; i < indexAttribute.count; i += 3) {
      const a = indexAttribute.getX(i)
      const b = indexAttribute.getX(i + 1)
      const c = indexAttribute.getX(i + 2)

      if (indexMap.has(a) && indexMap.has(b) && indexMap.has(c)) {
        newIndices.push(indexMap.get(a), indexMap.get(b), indexMap.get(c))
      }
    }

    // 更新几何体属性
    geometry.setAttribute('position', new Float32BufferAttribute(newPositions, 3))
    geometry.setAttribute('normal', new Float32BufferAttribute(newNormals, 3))
    geometry.setAttribute('uv', new Float32BufferAttribute(newUVs, 2))
    geometry.setIndex(newIndices)

    // geometry.addGroup(0, 6, 1);
    // geometry.addGroup(6, 6, 2);
    // geometry.addGroup(12, 6, 1);

    return geometry
  }
}
